# -*- coding: utf-8 -*-

import logging
import inspect
import json
import collections
from functools import wraps

from werkzeug.wrappers import BaseResponse as wzBaseResponse
from werkzeug.exceptions import HTTPException
from odoo import http
from odoo.service import model as service_model

from .exceptions import BaseOdooException
from . import parser
from . import marshal

logger = logging.getLogger(__name__)

JSON_METHODS = ['POST', 'PUT', 'DELETE', 'PATCH']


class ControllerType(http.ControllerType):
    def __init__(cls, name, bases, attrs):
        super().__init__(name, bases, attrs)


class Controller(http.Controller, metaclass=ControllerType):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.request = http.request


class WebRequest(http.WebRequest):
    def _call_function(self, *args, **kwargs):
        request = self

        if self.endpoint_arguments:
            kwargs.update(self.endpoint_arguments)

        # Backward for 7.0
        if self.endpoint.first_arg_is_req:
            args = (request,) + args

        # Correct exception handling and concurency retry
        @service_model.check
        def checked_call(___dbname, *a, **kw):
            # The decorator can call us more than once if there is an database error. In this
            # case, the request cursor is unusable. Rollback transaction to create a new one.
            if self._cr:
                self._cr.rollback()
                self.env.clear()
            result = self.endpoint(*a, **kw)
            if isinstance(result, http.Response) and result.is_qweb:
                # Early rendering of lazy responses to benefit from @service_model.check protection
                result.flatten()
            return result

        if self.db:
            return checked_call(self.db, *args, **kwargs)
        return self.endpoint(*args, **kwargs)

    def _retrieve_multidict(self, location='args'):
        if location not in ('args', 'form', 'files'):
            return {}
        md = getattr(self.httprequest, location)
        ret = {}
        for key in md:
            vals = md.getlist(key)
            if len(vals) == 1:
                vals = vals[0]
            ret[key] = vals
        return ret


class JsonRequest(WebRequest):
    _request_type = 'json'

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.params = self._retrieve_multidict()
        self.json_payload = self.params.copy()
        self.unjsonify_data = None
        if self.httprequest.method == 'POST' \
                and self.httprequest.environ.get('CONTENT_TYPE') == 'multipart/form-data':
            self.params.update(self._retrieve_multidict('form'))
            self.params.update(self._retrieve_multidict('files'))
        else:
            self.unjsonify_data = self.httprequest.get_data().decode(self.httprequest.charset)

    def dispatch(self):
        try:
            if self.unjsonify_data is not None:
                if self.unjsonify_data:
                    try:
                        self.params.update(json.loads(self.unjsonify_data))
                        self.unjsonify_data = None
                    except ValueError:
                        raise BaseOdooException(
                            http_code=400,
                            errcode=400,
                            message='Invalid JSON data',
                        )

            result = self._call_function(**self.params)
            if isinstance(result, wzBaseResponse):
                # odoo.http.route() would transform str to Response
                # this makes sense
                # so preserve this logic
                return result
            return JsonResponse(result)
        except Exception as e:
            import traceback
            if not isinstance(e, HTTPException):
                print(traceback.format_exc())
            return self._handle_exception(e)

    def _handle_exception(self, exception):
        if isinstance(exception, BaseOdooException):
            resp = JsonResponse(exception.data, status=exception.code)
        elif isinstance(exception, HTTPException):
            data = {
                'message': exception.description,
                'code': exception.code,
            }
            resp = JsonResponse(data, status=exception.code)
        else:
            data = {
                'message': 'Unexpected exception: {}'.format(exception),
                'code': 500,
            }
            resp = JsonResponse(data, status=500)
        return resp


class JsonResponse(http.Response):
    default_mimetype = 'application/json'

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.using_custom_handler = isinstance(http.request._get_current_object(), JsonRequest)

        if isinstance(self.response, (list, dict)):
            self.set_data(self.json_payload())

    def json_payload(self):
        if self.using_custom_handler:
            payload = self.response
        else:
            payload = {
                'jsonrpc': '2.0',
                'id': None,
                'result': self.response
            }
        return json.dumps(payload)


def route(route_arg=None, parse_with=None, marshal_with=None, **kwargs):
    methods = kwargs.get('methods', [])
    if 'type' not in kwargs:
        for method in methods:
            if method.upper() in JSON_METHODS:
                kwargs['type'] = 'json'
                break
    kwargs['custom_handler'] = True

    if 'auth' not in kwargs:
        kwargs['auth'] = 'wechat'

    if 'csrf' not in kwargs:
        kwargs['csrf'] = False

    routing = kwargs.copy()
    assert 'type' not in routing or routing['type'] in ("http", "json")

    def decorator(f):
        if inspect.isclass(f):
            if route_arg is not None:
                route_arg.rstrip('/')
            if not route_arg:
                return f

            assert route_arg.startswith('/')

            for k, v in f.__dict__.items():
                if k.startswith('__'):
                    continue
                if not inspect.isfunction(v) or not hasattr(v, 'routing'):
                    continue
                if not v.routing.get('routes'):
                    continue
                v.routing['routes'] = [(route_arg + r) for r in v.routing['routes']]

            f.url_prefix = route_arg
            return f

        if parse_with:
            f = parser.parse_with(parse_with)(f)
        if marshal_with:
            f = marshal.marshal_with(marshal_with)(f)
        f = transform_exc(f)

        return http.route(route_arg, **kwargs)(f)
    return decorator


def transform_exc(f):
    @wraps(f)
    def wrapper(*args, **kwargs):
        if isinstance(http.request._get_current_object(), JsonRequest):
            resp = f(*args, **kwargs)
        else:
            try:
                resp = f(*args, **kwargs)
            except BaseOdooException as e:
                if http.request.httprequest.method.upper() not in JSON_METHODS:
                    resp = JsonResponse(e.data, status=e.code)
                else:
                    resp = e.data
        return resp
    return wrapper
